package lpg.util;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.charset.Charset;

public class DispatchWriter extends PrintWriter
{
    public DispatchWriter()
    {
        this(new File("."));
    }

    public DispatchWriter(String outDirName)
    {
        this(new File(outDirName));
    }

    public DispatchWriter(File outDir)
    {
        this(new AdapterWriter(), outDir);
    }

    private DispatchWriter(AdapterWriter adapterWriter, File outDir)
    {
        super(adapterWriter, true); // Auto flush

        adapter = adapterWriter;
        adapter.setOutputDir(outDir);
    }

    public void setOutputDir(String outDirPath)
    {
        setOutputDir(new File(outDirPath));
    }

    public void setOutputDir(File outDir)
    {
        adapter.setOutputDir(outDir);
    }

    public void setOutputBuffer(StringBuilder buf)
    {
        adapter.setOutputBuffer(buf);
    }

    public void setCharset(Charset cs)
    {
        adapter.setCharset(cs);
    }

    public void beginAutoWrap()
    {
        adapter.beginAutoWrap();
    }

    public void endAutoWrap()
    {
        adapter.endAutoWrap();
    }

    public void setLineSize(int lineSize)
    {
        adapter.setLineSize(lineSize);
    }

    public void setIndentSize(int indentSize)
    {
        adapter.setIndentSize(indentSize);
    }

    public void setIndentChar(char indentChar)
    {
        adapter.setIndentChar(indentChar);
    }

    public void setSeparatorSize(int separatorSize)
    {
        adapter.setSeparatorSize(separatorSize);
    }

    public void setSeparatorChar(char separatorChar)
    {
        adapter.setSeparatorChar(separatorChar);
    }

    public int getCurrentColumn()
    {
        return adapter.getCurrentColumn();
    }

    public void padToColumn(int columnNo)
    {
        adapter.padToColumn(columnNo);
    }

    private final AdapterWriter adapter;

    private static class AdapterWriter extends Writer
    {
        public void write(char[] cbuf, int off, int len) throws IOException
        {
            if (autoWrap)
                wrap(cbuf, off, len);
            else
                process(cbuf, off, len);
        }

        //
        // PRINT_LARGE_TOKEN generates code to print a token that may exceed the
        // limit of its field.  The argument are LINE which is the symbol a varying
        // length character string, TOKEN which is the symbol to be printed, INDENT
        // which is a character string to be used as an initial prefix to indent the
        // output line, and LEN which indicates the maximum number of characters that
        // can be printed on a given line.  At the end of this process, LINE will
        // have the value of the remaining substring that can fit on the output line.
        // If a TOKEN is too large to be indented in a line, but not too large for
        // the whole line, we forget the indentation, and printed it. Otherwise, it
        // is "chapped up" and printed in pieces that are each indented.
        //
        private void wrap(char[] cbuf, int off, int len) throws IOException
        {
            int columnNo = getCurrentColumn();
            if (columnNo > 0)
            {
                int capacity = lineSize - columnNo - separatorSize;
                if (len <= capacity)
                {
                    printSeparator();
                    buffer.append(cbuf, off, len);
                    return;
                }
                if (len <= lineSize)
                {
                    processLine();
                    if (indentSize + len <= lineSize)
                        printIndent();
                    buffer.append(cbuf, off, len);
                    return;
                }
                if (capacity > 0)
                {
                    printSeparator();
                    buffer.append(cbuf, off, capacity);
                    off += capacity;
                    len -= capacity;
                }
                processLine();
            }
            else if (indentSize + len > lineSize && len <= lineSize)
            {
                buffer.append(cbuf, off, len);
                return;
            }

            for (int capacity = lineSize - indentSize;
                 len > capacity;
                 off += capacity, len -= capacity)
            {
                printIndent();
                buffer.append(cbuf, off, capacity);
                processLine();
            }
            printIndent();
            buffer.append(cbuf, off, len);
        }

        private void printSeparator()
        {
            for (int i = 0; i < separatorSize; i++)
                buffer.append(separatorChar);
        }

        private void printIndent()
        {
            for (int i = 0; i < indentSize; i++)
                buffer.append(indentChar);
        }

        private void process(char[] cbuf, int off, int len) throws IOException
        {
            for (int i = off, n = off + len; i < n; i++)
            {
                char c = cbuf[i];
                switch (c)
                {
                    case '\r':
                        skipLF = true;
                        processLine();
                        break;
                    case '\n':
                        if (skipLF)
                            skipLF = false;
                        else
                            processLine();
                        break;
                    default:
                        skipLF = false;
                        buffer.append(c);
                }
            }
        }

        public void flush() throws IOException
        {
            if (buffer.length() > 0)
                processLine();
            flushWriter();
        }

        private void flushWriter() throws IOException
        {
            if (currentWriter != null)
                currentWriter.flush();
        }

        public void close() throws IOException
        {
            try
            {
                flush();
            }
            finally
            {
                reset(null, false);
            }
        }

        private void processLine() throws IOException
        {
            // Reset buffer
            String line = new String(buffer);
            buffer = new StringBuilder();

            // Reset writer or write a line
            String oneLine = line.trim();
            if (oneLine.startsWith("//--"))
            {
                try
                {
                    flushWriter();
                }
                finally
                {
                    reset(oneLine.substring(4), false);
                }
            }
            else if (oneLine.startsWith("//-+"))
            {
                try
                {
                    flushWriter();
                }
                finally
                {
                    reset(oneLine.substring(4), true);
                }
            }
            else
            {
                if (outputBuffer != null)
                {
                    if (!autoWrap)
                        for (int i = 0; i < indentSize; i++)
                            outputBuffer.append(indentChar);
                    outputBuffer.append(line);
                    outputBuffer.append(Utility.LINE_SEPARATOR);
                }
                if (currentWriter != null)
                {
                    if (!autoWrap)
                        for (int i = 0; i < indentSize; i++)
                            currentWriter.write(indentChar);
                    currentWriter.write(line);
                    currentWriter.newLine();
                }
            }
        }

        private void reset(String fileName, boolean append) throws IOException
        {
            // Reset file handlers
            Utility.close(fos);
            fos = null;
            currentWriter = null;

            if (fileName == null)
                return;

            file = new File(fileName);
            if (!file.isAbsolute())
                file = new File(outputDir, fileName);

            // Validate output file
            if (file.exists())
            {
                if (file.isDirectory())
                    throw new IOException("\"" + file.getAbsolutePath() + "\" is a directory.");
                if (!file.canWrite())
                    throw new IOException("\"" + file.getAbsolutePath() + "\" is readonly.");
            }
            else
            {
                File parent = file.getParentFile();
                if (parent != null && !parent.exists() && !parent.mkdirs())
                    throw new IOException("Cannot make directory \"" + parent.getAbsolutePath() + "\".");
            }

            try
            {
                fos = new FileOutputStream(file, append);
                currentWriter = new BufferedWriter(new OutputStreamWriter(fos, charset));
            }
            finally
            {
                if (currentWriter == null)
                {
                    Utility.close(fos);
                    fos = null;
                }
            }
        }

        private void setOutputDir(File outDir)
        {
            outputDir = outDir;
        }

        private void setOutputBuffer(StringBuilder buf)
        {
            outputBuffer = buf;
        }

        private void setCharset(Charset cs)
        {
            if (cs == null)
                throw new NullPointerException("charset");

            charset = cs;
        }

        private void beginAutoWrap()
        {
            autoWrap = true;

            lineSize = 80;
            indentSize = 0;
            indentChar = ' ';
            separatorSize = 1;
            separatorChar = ' ';
        }

        private void endAutoWrap()
        {
            autoWrap = false;

            lineSize = 80;
            indentSize = 0;
            indentChar = ' ';
            separatorSize = 1;
            separatorChar = ' ';
        }

        private void setLineSize(int lineSize)
        {
            if (lineSize <= 0 || lineSize <= indentSize)
                throw new IllegalArgumentException("lineSize");

            this.lineSize = lineSize;
        }

        private void setIndentSize(int indentSize)
        {
            if (indentSize < 0 || lineSize <= indentSize)
                throw new IllegalArgumentException("indentSize");

            this.indentSize = indentSize;
        }

        private void setIndentChar(char indentChar)
        {
            this.indentChar = indentChar;
        }

        private void setSeparatorSize(int separatorSize)
        {
            if (separatorSize < 0)
                throw new IllegalArgumentException("separatorSize");

            this.separatorSize = separatorSize;
        }

        private void setSeparatorChar(char separatorChar)
        {
            this.separatorChar = separatorChar;
        }

        private int getCurrentColumn()
        {
            return buffer.length();
        }

        private void padToColumn(int columnNo)
        {
            if (columnNo < 0)
                throw new IllegalArgumentException("columnNo");

            int currentColumnNo = getCurrentColumn();
            if (currentColumnNo == 0)
                for (; currentColumnNo < columnNo; currentColumnNo++)
                    buffer.append(indentChar);
            else
                for (; currentColumnNo < columnNo; currentColumnNo++)
                    buffer.append(separatorChar);
        }

        private DispatchWriter dWriter = null;

        private File outputDir = null;
        private Charset charset = Utility.getCharset();
        private StringBuilder outputBuffer = null;

        private StringBuilder buffer = new StringBuilder();
        private boolean skipLF = false;

        private BufferedWriter currentWriter = null;
        private FileOutputStream fos = null;

        private File file = null;

        private boolean autoWrap = false;
        private int lineSize = 80;
        private int indentSize = 0;
        private char indentChar = ' ';
        private int separatorSize = 1;
        private char separatorChar = ' ';

    }

}
